/**
 * Copyright (c) 2012 committers of YAKINDU and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * Contributors:
 * 	committers of YAKINDU - initial API and implementation
 * 
 */

package org.yakindu.sct.model.sgraph.resource;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.impl.EClassImpl;
import org.eclipse.emf.ecore.util.EContentAdapter;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.InternalEList;
import org.eclipse.emf.ecore.xmi.impl.XMIResourceImpl;
import org.eclipse.xtext.Constants;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.ParserRule;
import org.eclipse.xtext.XtextFactory;
import org.eclipse.xtext.diagnostics.DiagnosticMessage;
import org.eclipse.xtext.diagnostics.Severity;
import org.eclipse.xtext.linking.ILinker;
import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider;
import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext;
import org.eclipse.xtext.linking.ILinkingService;
import org.eclipse.xtext.linking.impl.LinkingHelper;
import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic;
import org.eclipse.xtext.linking.lazy.LazyURIEncoder;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.parser.IParseResult;
import org.eclipse.xtext.parser.IParser;
import org.eclipse.xtext.parsetree.reconstr.XtextSerializationException;
import org.eclipse.xtext.resource.XtextSyntaxDiagnostic;
import org.eclipse.xtext.resource.impl.ListBasedDiagnosticConsumer;
import org.eclipse.xtext.serializer.ISerializer;
import org.eclipse.xtext.util.CancelIndicator;
import org.eclipse.xtext.util.OnChangeEvictingCache;
import org.eclipse.xtext.util.Triple;
import org.eclipse.xtext.validation.IConcreteSyntaxValidator;
import org.yakindu.sct.model.sgraph.SGraphPackage;
import org.yakindu.sct.model.sgraph.SpecificationElement;
import org.yakindu.sct.model.sgraph.State;
import org.yakindu.sct.model.sgraph.Statechart;
import org.yakindu.sct.model.sgraph.Transition;

import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
 * 
 * @author andreas muelder - Initial contribution and API
 * 
 */
public abstract class AbstractSCTResource extends XMIResourceImpl {

	public static final String SCT_PREFIX = "SCT_";

	@Inject
	private IParser parser;
	@Inject
	private ILinker linker;
	@Inject
	private ISerializer serializer;
	@Inject
	private LazyURIEncoder encoder;
	@Inject
	private ILinkingService linkingService;
	@Inject
	private ILinkingDiagnosticMessageProvider diagnosticMessageProvider;
	@Inject
	private LinkingHelper linkingHelper;
	@Inject
	private OnChangeEvictingCache cache;
	@Inject
	@Named(Constants.LANGUAGE_NAME)
	private String languageName;

	protected boolean isParsing = false;

	protected boolean isLinking = false;

	protected boolean isSerializing = false;

	private boolean serializerEnabled = false;

	protected Multimap<SpecificationElement, Diagnostic> syntaxDiagnostics;

	protected Multimap<SpecificationElement, Diagnostic> linkingDiagnostics;

	protected Map<SpecificationElement, Diagnostic> sublinkDiagnostics;

	protected abstract void parseTransition(Transition element);

	protected abstract void parseState(State element);

	protected abstract void parseStatechart(Statechart element);

	protected abstract void serializeStatechart(Statechart element);

	protected abstract void serializeState(State element);

	protected abstract void serializeTransition(Transition element);

	public AbstractSCTResource(URI uri) {
		super(uri);
		syntaxDiagnostics = HashMultimap.create();
		linkingDiagnostics = HashMultimap.create();
		setIntrinsicIDToEObjectMap(new HashMap<String, EObject>());
	}
	
	@Override
	public void doLoad(java.io.InputStream inputStream, Map<?, ?> options) throws IOException {
		// mute notifications to avoid cache clearing
		cache.getOrCreate(this).ignoreNotifications();
		super.doLoad(inputStream, options);
		cache.getOrCreate(this).listenToNotifications();
	}

	@Override
	protected boolean useUUIDs() {
		return true;
	}

	@Override
	protected boolean useIDAttributes() {
		return true;
	}

	@Override
	protected void attachedHelper(EObject eObject) {
		super.attachedHelper(eObject);
		if (eObject instanceof SpecificationElement) {
			Adapter parseAdapter = EcoreUtil.getExistingAdapter(eObject, ParseAdapter.class);
			if (parseAdapter == null) {
				parseSpecificationElement((SpecificationElement) eObject);
				linkSpecificationElement((SpecificationElement) eObject);
				eObject.eAdapters().add(new ParseAdapter());
			}
			Adapter serializeAdapter = EcoreUtil.getExistingAdapter(eObject, SerializeAdapter.class);
			if (serializeAdapter == null) {
				eObject.eAdapters().add(new SerializeAdapter());
			}
		}
	}

	@Override
	public void detached(EObject eObject) {
		super.detached(eObject);
		DETACHED_EOBJECT_TO_ID_MAP.clear();
	}

	@Override
	protected void detachedHelper(EObject eObject) {
		if (eObject instanceof SpecificationElement) {
			Adapter parseAdapter = EcoreUtil.getExistingAdapter(eObject, ParseAdapter.class);
			if (parseAdapter != null) {
				eObject.eAdapters().remove(parseAdapter);
			}
			Adapter serializeAdapter = EcoreUtil.getExistingAdapter(eObject, SerializeAdapter.class);
			if (serializeAdapter != null) {
				eObject.eAdapters().remove(serializeAdapter);
			}
		}
		super.detachedHelper(eObject);
	}

	@Override
	public EObject getEObject(String uriFragment) {
		if (encoder.isCrossLinkFragment(this, uriFragment)) {
			Triple<EObject, EReference, INode> triple = encoder.decode(this, uriFragment);
			List<EObject> linkedObjects = null;
			linkedObjects = linkingService.getLinkedObjects(triple.getFirst(), triple.getSecond(), triple.getThird());
			if (!linkedObjects.isEmpty()) {
				return linkedObjects.get(0);
			} else {
				createDiagnostic(triple);
				return null;
			}
		}
		if (uriFragment != null && uriFragment.startsWith(SCT_PREFIX)) {
			return super.getEObject(uriFragment.substring(SCT_PREFIX.length()));
		}
		return super.getEObject(uriFragment);
	}

	@Override
	public String getURIFragment(EObject eObject) {
		if (unloadingContents != null) {
			return super.getURIFragment(eObject);
		}
		ICompositeNode node = NodeModelUtils.findActualNodeFor(eObject);
		if (node != null) {
			String fragment = super.getURIFragment(eObject);
			if (!Strings.isNullOrEmpty(fragment)) {
				return SCT_PREFIX + fragment;
			}
		}
		return super.getURIFragment(eObject);
	}

	@Override
	public String getID(EObject eObject) {
		if (NodeModelUtils.getNode(eObject) != null)
			return null;
		return super.getID(eObject);
	}

	protected void createDiagnostic(Triple<EObject, EReference, INode> triple) {
		SpecificationElement specificationElement = EcoreUtil2.getContainerOfType(triple.getFirst(),
				SpecificationElement.class);
		DiagnosticMessage message = createDiagnosticMessage(triple);
		Diagnostic diagnostic = new XtextLinkingDiagnostic(triple.getThird(), message.getMessage(),
				message.getIssueCode(), message.getIssueData());
		linkingDiagnostics.put(specificationElement, diagnostic);

	}

	protected DiagnosticMessage createDiagnosticMessage(Triple<EObject, EReference, INode> triple) {
		ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext context = createDiagnosticMessageContext(triple);
		DiagnosticMessage message = diagnosticMessageProvider.getUnresolvedProxyMessage(context);
		return message;
	}

	protected ILinkingDiagnosticContext createDiagnosticMessageContext(Triple<EObject, EReference, INode> triple) {
		return new DiagnosticMessageContext(triple, linkingHelper);
	}

	// copied from xtext LazyLinkingResource
	public void resolveLazyCrossReferences(CancelIndicator monitor) {
		TreeIterator<Object> iterator = EcoreUtil.getAllContents(this, true);
		while (iterator.hasNext()) {
			InternalEObject source = (InternalEObject) iterator.next();
			EStructuralFeature[] eStructuralFeatures = ((EClassImpl.FeatureSubsetSupplier) source.eClass()
					.getEAllStructuralFeatures()).crossReferences();
			if (eStructuralFeatures != null) {
				for (EStructuralFeature crossRef : eStructuralFeatures) {
					if (monitor.isCanceled()) {
						return;
					}
					resolveLazyCrossReference(source, crossRef, monitor);
				}
			}
		}
	}

	// copied from xtext LazyLinkingResource
	protected void resolveLazyCrossReference(InternalEObject source, EStructuralFeature crossRef,
			CancelIndicator monitor) {
		if (crossRef.isDerived())
			return;
		if (crossRef.isMany()) {
			@SuppressWarnings("unchecked")
			InternalEList<EObject> list = (InternalEList<EObject>) source.eGet(crossRef);
			for (int i = 0; i < list.size(); i++) {
				EObject proxy = list.basicGet(i);
				if (proxy.eIsProxy()) {
					URI proxyURI = ((InternalEObject) proxy).eProxyURI();
					if (getURI().equals(proxyURI.trimFragment())) {
						final String fragment = proxyURI.fragment();
						if (encoder.isCrossLinkFragment(this, fragment) && !monitor.isCanceled()) {
							EObject target = getEObject(fragment);
							if (target != null) {
								try {
									source.eSetDeliver(false);
									list.setUnique(i, target);
								} finally {
									source.eSetDeliver(true);
								}
							}
						}
					}
				}
			}
		} else {
			EObject proxy = (EObject) source.eGet(crossRef, false);
			if (proxy != null && proxy.eIsProxy()) {
				URI proxyURI = ((InternalEObject) proxy).eProxyURI();
				if (getURI().equals(proxyURI.trimFragment())) {
					final String fragment = proxyURI.fragment();
					if (encoder.isCrossLinkFragment(this, fragment) && !monitor.isCanceled()) {
						EObject target = getEObject(fragment);
						if (target != null) {
							try {
								source.eSetDeliver(false);
								source.eSet(crossRef, target);
							} finally {
								source.eSetDeliver(true);
							}
						}
					}
				}
			}
		}
	}

	public void parseSpecificationElement(SpecificationElement element) {
		Assert.isNotNull(element);
		isParsing = true;
		if (element instanceof Transition) {
			parseTransition((Transition) element);
		} else if (element instanceof State) {
			parseState((State) element);
		} else if (element instanceof Statechart) {
			parseStatechart((Statechart) element);
		}
		isParsing = false;
	}

	protected IParseResult parse(SpecificationElement element, String rule) {
		ParserRule parserRule = XtextFactory.eINSTANCE.createParserRule();
		parserRule.setName(rule);
		String specification = element.getSpecification();
		IParseResult result = parser.parse(parserRule, new StringReader(specification != null ? specification : ""));
		createDiagnostics(result, element);
		return result;
	}

	protected void createDiagnostics(IParseResult parseResult, SpecificationElement semanticTarget) {
		syntaxDiagnostics.get(semanticTarget).clear();
		for (INode error : parseResult.getSyntaxErrors()) {
			syntaxDiagnostics.put(semanticTarget, new XtextSyntaxDiagnostic(error));
		}
	}

	public void linkSpecificationElements() {
		TreeIterator<EObject> iter = getAllContents();
		while (iter.hasNext()) {
			EObject currentObject = iter.next();
			if (currentObject instanceof SpecificationElement) {
				linkSpecificationElement((SpecificationElement) currentObject);
			}
		}
	}
	
	public void parseSpecificationElements() {
		TreeIterator<EObject> iter = getAllContents();
		while (iter.hasNext()) {
			EObject currentObject = iter.next();
			if (currentObject instanceof SpecificationElement) {
				parseSpecificationElement((SpecificationElement) currentObject);
			}
		}
	}

	protected void linkSpecificationElement(SpecificationElement element) {
		isLinking = true;
		final ListBasedDiagnosticConsumer consumer = new ListBasedDiagnosticConsumer();
		linker.linkModel(element, consumer);
		linkingDiagnostics.get(element).clear();
		linkingDiagnostics.putAll(element, (consumer.getResult(Severity.ERROR)));
		linkingDiagnostics.putAll(element, (consumer.getResult(Severity.WARNING)));
		isLinking = false;
	}

	protected void serializeSpecificationElement(SpecificationElement element) {
		if (getSyntaxDiagnostics().get(element).size() > 0 || getLinkingDiagnostics().get(element).size() > 0) {
			return;
		}
		try {
			isSerializing = true;
			if (element instanceof Transition) {
				serializeTransition((Transition) element);
			} else if (element instanceof State) {
				serializeState((State) element);
			} else if (element instanceof Statechart) {
				serializeStatechart((Statechart) element);
			}
		} catch (XtextSerializationException ex) {
			ex.printStackTrace();
			// Leave the old specification
		} catch (IConcreteSyntaxValidator.InvalidConcreteSyntaxException ex) {
			ex.printStackTrace();
			// Leave the old specification
		} finally {
			isSerializing = false;
		}
	}

	protected String serialize(EObject object) {
		if (object != null) {
			return serializer.serialize(object);
		}
		return "";
	}

	public Multimap<SpecificationElement, Diagnostic> getSyntaxDiagnostics() {
		return syntaxDiagnostics;
	}

	public Multimap<SpecificationElement, Diagnostic> getLinkingDiagnostics() {
		return linkingDiagnostics;
	}

	public String getLanguageName() {
		return languageName;
	}

	protected final class SerializeAdapter extends EContentAdapter {

		@Override
		public void notifyChanged(Notification msg) {
			super.notifyChanged(msg);
			if (isSerializerEnabled()) {
				if (isLoading() || isParsing || isLinking || isSerializing) {
					return;
				}
				if (msg.getEventType() == Notification.REMOVING_ADAPTER || msg.getEventType() == Notification.RESOLVE) {
					return;
				}
				Object notifier = msg.getNotifier();
				if (notifier instanceof EObject) {
					EObject eObject = (EObject) notifier;
					SpecificationElement container = EcoreUtil2.getContainerOfType(eObject, SpecificationElement.class);
					if (container != null) {
						serializeSpecificationElement(container);
					}
				}
			}
		}

		@Override
		public boolean isAdapterForType(Object type) {
			return SerializeAdapter.class == type;
		}
	}

	protected final class ParseAdapter extends AdapterImpl {

		@Override
		public void notifyChanged(Notification msg) {
			if (isSerializing) {
				return;
			}
			if (msg.getFeature() == SGraphPackage.Literals.SPECIFICATION_ELEMENT__SPECIFICATION) {
				String newValString = msg.getNewStringValue();
				String oldVString = msg.getOldStringValue();
				if (newValString != null && !newValString.equals(oldVString)) {
					parseSpecificationElement((SpecificationElement) msg.getNotifier());
					linkSpecificationElements();
				}
			}
		}

		@Override
		public boolean isAdapterForType(Object type) {
			return ParseAdapter.class == type;
		}
	}

	public boolean isSerializerEnabled() {
		return serializerEnabled;
	}

	public void setSerializerEnabled(boolean serializerEnabled) {
		this.serializerEnabled = serializerEnabled;
	}

	// copied from xtext LazyLinkingResource
	public static class DiagnosticMessageContext
			implements ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext {

		private final Triple<EObject, EReference, INode> triple;
		private final LinkingHelper linkingHelper;

		protected DiagnosticMessageContext(Triple<EObject, EReference, INode> triple, LinkingHelper helper) {
			this.triple = triple;
			this.linkingHelper = helper;
		}

		public EObject getContext() {
			return triple.getFirst();
		}

		public EReference getReference() {
			return triple.getSecond();
		}

		public String getLinkText() {
			return linkingHelper.getCrossRefNodeAsString(triple.getThird(), true);
		}

	}

	/**
	 * overridden because original calls 'Util.denormalizeURI(uri,
	 * getResourceSet())' which leads into error if there is no platform
	 * available
	 */
	@Override
	public void setURI(URI uri) {
		if (getResourceSet() != null) {
			setRawURI(uri);
		}
	}

	public void setRawURI(URI uri) {
		URI oldURI = getURI();
		if ((uri == oldURI) || ((uri != null) && (uri.equals(oldURI))))
			return;

		super.setURI(uri);
	}
}
